/*
 * Fuel UX Tree
 * https://github.com/ExactTarget/fuelux
 *
 * Copyright (c) 2014 ExactTarget
 * Licensed under the BSD New license.
 */

// -- BEGIN UMD WRAPPER PREFACE --

// For more information on UMD visit:
// https://github.com/umdjs/umd/blob/master/jqueryPlugin.js

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // if AMD loader is available, register as an anonymous module.
        define(['jquery'], factory);
    } else {
        // OR use browser globals if AMD is not present
        factory(jQuery);
    }
}(function ($) {
    // -- END UMD WRAPPER PREFACE --

    // -- BEGIN MODULE CODE HERE --

    var old = $.fn.tree;

    // TREE CONSTRUCTOR AND PROTOTYPE

    var Tree = function Tree(element, options) {
        this.$element = $(element);
        this.options = $.extend({}, $.fn.tree.defaults, options);

        if (this.options.itemSelect) {
            this.$element.on('click.fu.tree', '.tree-item', $.proxy(function (ev) {
                this.selectItem(ev.currentTarget);
            }, this));
        }

        this.$element.on('click.fu.tree', '.tree-branch-name', $.proxy(function (ev) {
            this.toggleFolder(ev.currentTarget);
        }, this));

        // folderSelect default is true
        if (this.options.folderSelect) {
            this.$element.addClass('tree-folder-select');
            this.$element.off('click.fu.tree', '.tree-branch-name');
            this.$element.on('click.fu.tree', '.icon-caret', $.proxy(function (ev) {
                this.toggleFolder($(ev.currentTarget).parent());
            }, this));
            this.$element.on('click.fu.tree', '.tree-branch-name', $.proxy(function (ev) {
                this.selectFolder($(ev.currentTarget));
            }, this));
        }

        this.render();
    };

    Tree.prototype = {
        constructor: Tree,

        deselectAll: function deselectAll(nodes) {
            // clear all child tree nodes and style as deselected
            nodes = nodes || this.$element;
            var $selectedElements = $(nodes).find('.tree-selected');
            $selectedElements.each(function (index, element) {
                styleNodeDeselected( $(element), $(element).find( '.glyphicon' ) );
            });
            return $selectedElements;
        },

        destroy: function destroy() {
            // any external bindings [none]
            // empty elements to return to original markup
            this.$element.find("li:not([data-template])").remove();

            this.$element.remove();
            // returns string of markup
            return this.$element[0].outerHTML;
        },

        render: function render() {
            this.populate(this.$element);
        },

        populate: function populate($el) {
            var self = this;
            var $parent = ($el.hasClass('tree')) ? $el : $el.parent();
            var loader = $parent.find('.tree-loader:eq(0)');
            var treeData = $parent.data();

            loader.removeClass('hide hidden'); // hide is deprecated
            this.options.dataSource(treeData ? treeData : {}, function (items) {
                loader.addClass('hidden');

                $.each(items.data, function (index, value) {
                    var $entity;

                    if (value.type === 'folder') {
                        $entity = self.$element.find('[data-template=treebranch]:eq(0)').clone().removeClass('hide hidden').removeData('template'); // hide is deprecated
                        $entity.data(value);
                        $entity.find('.tree-branch-name > .tree-label').html(value.text || value.name);
                    } else if (value.type === 'item') {
                        $entity = self.$element.find('[data-template=treeitem]:eq(0)').clone().removeClass('hide hidden').removeData('template'); // hide is deprecated
                        $entity.find('.tree-item-name > .tree-label').html(value.text || value.name);
                        $entity.data(value);
                    }

                    // Decorate $entity with data or other attributes making the
                    // element easily accessable with libraries like jQuery.
                    //
                    // Values are contained within the object returned
                    // for folders and items as attr:
                    //
                    // {
                    //     text: "An Item",
                    //     type: 'item',
                    //     attr = {
                    //         'classes': 'required-item red-text',
                    //         'data-parent': parentId,
                    //         'guid': guid,
                    //         'id': guid
                    //     }
                    // };
                    //
                    // the "name" attribute is also supported but is deprecated for "text".

                    // add attributes to tree-branch or tree-item
                    var attr = value.attr || value.dataAttributes || [];
                    $.each(attr, function (key, value) {
                        switch (key) {
                            case 'cssClass':
                            case 'class':
                            case 'className':
                                $entity.addClass(value);
                                break;

                            // allow custom icons
                            case 'data-icon':
                                $entity.find('.icon-item').removeClass().addClass('icon-item ' + value);
                                $entity.attr(key, value);
                                break;

                            // ARIA support
                            case 'id':
                                $entity.attr(key, value);
                                $entity.attr('aria-labelledby', value + '-label');
                                $entity.find('.tree-branch-name > .tree-label').attr('id', value + '-label');
                                break;

                            // style, data-*
                            default:
                                $entity.attr(key, value);
                                break;
                        }
                    });

                    // add child nodes
                    if ($el.hasClass('tree-branch-header')) {
                        $parent.find('.tree-branch-children:eq(0)').append($entity);
                    } else {
                        $el.append($entity);
                    }
                });

                // return newly populated folder
                self.$element.trigger('loaded.fu.tree', $parent);
            });
        },

        selectTreeNode: function selectItem(clickedElement, nodeType) {
            var clicked = {};	// object for clicked element
            clicked.$element = $(clickedElement);

            var selected = {}; // object for selected elements
            selected.$elements = this.$element.find('.tree-selected');
            selected.dataForEvent = [];

            // determine clicked element and it's icon
            if (nodeType === 'folder') {
                // make the clicked.$element the container branch
                clicked.$element = clicked.$element.closest('.tree-branch');
                clicked.$icon = clicked.$element.find('.icon-folder');
            }
            else {
                clicked.$icon = clicked.$element.find('.icon-item');
            }
            clicked.elementData = clicked.$element.data();

            // the below functions pass objects by copy/reference and use modified object in this function
            if ( this.options.multiSelect ) {
                multiSelectSyncNodes(this, clicked, selected);
            }
            else {
                singleSelectSyncNodes(this, clicked, selected);
            }

            // all done with the DOM, now fire events
            this.$element.trigger(selected.eventType + '.fu.tree', {
                target: clicked.elementData,
                selected: selected.dataForEvent
            });

            clicked.$element.trigger('updated.fu.tree', {
                selected: selected.dataForEvent,
                item: clicked.$element,
                eventType: selected.eventType
            });
        },

        discloseFolder: function discloseFolder(el) {
            var $el = $(el);

            var $branch = $el.closest('.tree-branch');
            var $treeFolderContent = $branch.find('.tree-branch-children');
            var $treeFolderContentFirstChild = $treeFolderContent.eq(0);

            //take care of the styles
            $branch.addClass('tree-open');
            $branch.attr('aria-expanded', 'true');
            $treeFolderContentFirstChild.removeClass('hide hidden'); // hide is deprecated
            $branch.find('> .tree-branch-header .icon-folder').eq(0)
                .removeClass('glyphicon-folder-close')
                .addClass('glyphicon-folder-open');

            //add the children to the folder
            if (!$treeFolderContent.children().length) {
                this.populate($treeFolderContent);
            }

            this.$element.trigger('disclosedFolder.fu.tree', $branch.data());
        },

        closeFolder: function closeFolder(el) {
            var $el = $(el);
            var $branch = $el.closest('.tree-branch');
            var $treeFolderContent = $branch.find('.tree-branch-children');
            var $treeFolderContentFirstChild = $treeFolderContent.eq(0);

            //take care of the styles
            $branch.removeClass('tree-open');
            $branch.attr('aria-expanded', 'false');
            $treeFolderContentFirstChild.addClass('hidden');
            $branch.find('> .tree-branch-header .icon-folder').eq(0)
                .removeClass('glyphicon-folder-open')
                .addClass('glyphicon-folder-close');

            // remove chidren if no cache
            if (!this.options.cacheItems) {
                $treeFolderContentFirstChild.empty();
            }

            this.$element.trigger('closed.fu.tree', $branch.data());
        },

        toggleFolder: function toggleFolder(el) {
            var $el = $(el);

            if ($el.find('.glyphicon-folder-close').length) {
                this.discloseFolder(el);
            } else if ($el.find('.glyphicon-folder-open').length) {
                this.closeFolder(el);
            }
        },

        selectFolder: function selectFolder(el) {
            if (this.options.folderSelect) {
                this.selectTreeNode(el, 'folder');
            }
        },

        selectItem: function selectItem(el) {
            if (this.options.itemSelect) {
                this.selectTreeNode(el, 'item');
            }
        },

        selectedItems: function selectedItems() {
            var $sel = this.$element.find('.tree-selected');
            var data = [];

            $.each($sel, function (index, value) {
                data.push($(value).data());
            });
            return data;
        },

        // collapses open folders
        collapse: function collapse() {
            var self = this;
            var reportedClosed = [];

            var closedReported = function closedReported(event, closed) {
                reportedClosed.push(closed);

                // hide is deprecated
                if (self.$element.find(".tree-branch.tree-open:not('.hidden, .hide')").length === 0) {
                    self.$element.trigger('closedAll.fu.tree', {
                        tree: self.$element,
                        reportedClosed: reportedClosed
                    });
                    self.$element.off('loaded.fu.tree', self.$element, closedReported);
                }
            };

            //trigger callback when all folders have reported closed
            self.$element.on('closed.fu.tree', closedReported);

            self.$element.find(".tree-branch.tree-open:not('.hidden, .hide')").each(function () {
                self.closeFolder(this);
            });
        },

        //disclose visible will only disclose visible tree folders
        discloseVisible: function discloseVisible() {
            var self = this;

            var $openableFolders = self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')");
            var reportedOpened = [];

            var openReported = function openReported(event, opened) {
                reportedOpened.push(opened);

                if (reportedOpened.length === $openableFolders.length) {
                    self.$element.trigger('disclosedVisible.fu.tree', {
                        tree: self.$element,
                        reportedOpened: reportedOpened
                    });
                    /*
                     * Unbind the `openReported` event. `discloseAll` may be running and we want to reset this
                     * method for the next iteration.
                     */
                    self.$element.off('loaded.fu.tree', self.$element, openReported);
                }
            };

            //trigger callback when all folders have reported opened
            self.$element.on('loaded.fu.tree', openReported);

            // open all visible folders
            self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')").each(function triggerOpen() {
                self.discloseFolder($(this).find('.tree-branch-header'));
            });
        },

        /**
         * Disclose all will keep listening for `loaded.fu.tree` and if `$(tree-el).data('ignore-disclosures-limit')`
         * is `true` (defaults to `true`) it will attempt to disclose any new closed folders than were
         * loaded in during the last disclosure.
         */
        discloseAll: function discloseAll() {
            var self = this;

            //first time
            if (typeof self.$element.data('disclosures') === 'undefined') {
                self.$element.data('disclosures', 0);
            }

            var isExceededLimit = (self.options.disclosuresUpperLimit >= 1 && self.$element.data('disclosures') >= self.options.disclosuresUpperLimit);
            var isAllDisclosed = self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')").length === 0;


            if (!isAllDisclosed) {
                if (isExceededLimit) {
                    self.$element.trigger('exceededDisclosuresLimit.fu.tree', {
                        tree: self.$element,
                        disclosures: self.$element.data('disclosures')
                    });

                    /*
                     * If you've exceeded the limit, the loop will be killed unless you
                     * explicitly ignore the limit and start the loop again:
                     *
                     *    $tree.one('exceededDisclosuresLimit.fu.tree', function () {
                     *        $tree.data('ignore-disclosures-limit', true);
                     *        $tree.tree('discloseAll');
                     *    });
                     */
                    if (!self.$element.data('ignore-disclosures-limit')) {
                        return;
                    }

                }

                self.$element.data('disclosures', self.$element.data('disclosures') + 1);

                /*
                 * A new branch that is closed might be loaded in, make sure those get handled too.
                 * This attachment needs to occur before calling `discloseVisible` to make sure that
                 * if the execution of `discloseVisible` happens _super fast_ (as it does in our QUnit tests
                 * this will still be called. However, make sure this only gets called _once_, because
                 * otherwise, every single time we go through this loop, _another_ event will be bound
                 * and then when the trigger happens, this will fire N times, where N equals the number
                 * of recursive `discloseAll` executions (instead of just one)
                 */
                self.$element.one('disclosedVisible.fu.tree', function () {
                    self.discloseAll();
                });

                /*
                 * If the page is very fast, calling this first will cause `disclosedVisible.fu.tree` to not
                 * be bound in time to be called, so, we need to call this last so that the things bound
                 * and triggered above can have time to take place before the next execution of the
                 * `discloseAll` method.
                 */
                self.discloseVisible();
            } else {
                self.$element.trigger('disclosedAll.fu.tree', {
                    tree: self.$element,
                    disclosures: self.$element.data('disclosures')
                });

                //if `cacheItems` is false, and they call closeAll, the data is trashed and therefore
                //disclosures needs to accurately reflect that
                if (!self.options.cacheItems) {
                    self.$element.one('closeAll.fu.tree', function () {
                        self.$element.data('disclosures', 0);
                    });
                }

            }
        }
    };


    // ALIASES

    //alias for collapse for consistency. "Collapse" is an ambiguous term (collapse what? All? One specific branch?)
    Tree.prototype.closeAll = Tree.prototype.collapse;
    //alias for backwards compatibility because there's no reason not to.
    Tree.prototype.openFolder = Tree.prototype.discloseFolder;


    // PRIVATE FUNCTIONS

    function styleNodeSelected ($element, $icon) {
        $element.addClass('tree-selected');
        if ( $element.data('type') === 'item' && $icon.hasClass('ace-icon fa fa-times') ) {
            $icon.removeClass('ace-icon fa fa-times').addClass('ace-icon fa fa-check'); // make checkmark
        }
    }

    function styleNodeDeselected ($element, $icon) {
        $element.removeClass('tree-selected');
        if ( $element.data('type') === 'item' && $icon.hasClass('ace-icon fa fa-check') ) {
            $icon.removeClass('ace-icon fa fa-check').addClass('ace-icon fa fa-times'); // make bullet
        }
    }

    function multiSelectSyncNodes (self, clicked, selected) {
        // search for currently selected and add to selected data list if needed
        $.each(selected.$elements, function (index, element) {
            var $element = $(element);
            if ($element[0] !== clicked.$element[0]) {
                selected.dataForEvent.push( $($element).data() );
            }
        });

        if (clicked.$element.hasClass('tree-selected')) {
            styleNodeDeselected (clicked.$element, clicked.$icon);
            // set event data
            selected.eventType = 'deselected';
        }
        else {
            styleNodeSelected(clicked.$element, clicked.$icon);
            // set event data
            selected.eventType = 'selected';
            selected.dataForEvent.push(clicked.elementData);
        }
    }

    function singleSelectSyncNodes(self, clicked, selected) {
        // element is not currently selected
        if (selected.$elements[0] !== clicked.$element[0]) {
            var clearedElements = self.deselectAll(self.$element);
            styleNodeSelected(clicked.$element, clicked.$icon);
            // set event data
            selected.eventType = 'selected';
            selected.dataForEvent = [clicked.elementData];
        }
        else {
            styleNodeDeselected(clicked.$element, clicked.$icon);
            // set event data
            selected.eventType = 'deselected';
            selected.dataForEvent = [];
        }
    }


    // TREE PLUGIN DEFINITION

    $.fn.tree = function tree(option) {
        var args = Array.prototype.slice.call(arguments, 1);
        var methodReturn;

        var $set = this.each(function () {
            var $this = $(this);
            var data = $this.data('fu.tree');
            var options = typeof option === 'object' && option;

            if (!data) {
                $this.data('fu.tree', (data = new Tree(this, options)));
            }

            if (typeof option === 'string') {
                methodReturn = data[option].apply(data, args);
            }
        });

        return (methodReturn === undefined) ? $set : methodReturn;
    };

    $.fn.tree.defaults = {
        dataSource: function dataSource(options, callback) {},
        multiSelect: false,
        cacheItems: true,
        folderSelect: true,
        itemSelect: true,
        /*
         * How many times `discloseAll` should be called before a stopping and firing
         * an `exceededDisclosuresLimit` event. You can force it to continue by
         * listening for this event, setting `ignore-disclosures-limit` to `true` and
         * starting `discloseAll` back up again. This lets you make more decisions
         * about if/when/how/why/how many times `discloseAll` will be started back
         * up after it exceeds the limit.
         *
         *    $tree.one('exceededDisclosuresLimit.fu.tree', function () {
         *        $tree.data('ignore-disclosures-limit', true);
         *        $tree.tree('discloseAll');
         *    });
         *
         * `disclusuresUpperLimit` defaults to `0`, so by default this trigger
         * will never fire. The true hard the upper limit is the browser's
         * ability to load new items (i.e. it will keep loading until the browser
         * falls over and dies). On the Fuel UX `index.html` page, the point at
         * which the page became super slow (enough to seem almost unresponsive)
         * was `4`, meaning 256 folders had been opened, and 1024 were attempting to open.
         */
        disclosuresUpperLimit: 0
    };

    $.fn.tree.Constructor = Tree;

    $.fn.tree.noConflict = function () {
        $.fn.tree = old;
        return this;
    };


    // NO DATA-API DUE TO NEED OF DATA-SOURCE

    // -- BEGIN UMD WRAPPER AFTERWORD --
}));
// -- END UMD WRAPPER AFTERWORD --
